package com.terrapages.wmsc4j.layer;

import java.awt.Dimension;
import java.awt.image.BufferedImage;

import org.apache.log4j.Logger;

import com.terrapages.wmsc4j.bean.BBox;
import com.terrapages.wmsc4j.bean.Pos;
import com.terrapages.wmsc4j.cache.Cache;
import com.terrapages.wmsc4j.layer.resolution.Resolution;
import com.terrapages.wmsc4j.layer.resolution.ResolutionSet;
import com.terrapages.wmsc4j.layer.tile.Tile;
import com.terrapages.wmsc4j.layer.tile.TileFormat;
import com.terrapages.wmsc4j.wms.WMSService;

/**
 * Layer uses meta-tiling when writting tiles to cache. Meta tiling is taking a big image
 * and splitting it up into respective tiles to cache. 
 * This can help things like labeling issues of Map Servers when the labels want to be drawn where tiles intercect
 * The key configuration of meta tiling is the multiplicity attribute.
 * A mutiplicity of
 * <ol>
 * 	<li> no meta-tileing
 * 	<li> 4 tiles in one meta tile
 * 	<li> 9 tiles in one meta tile
 * </ol>
 * etc ... mutiplicity of n means n^2 tiles in one meta tile
 * @author mvivian
 *
 */
public class WMSMetaLayer extends AbstractLiveLayer {

	protected int multiplicity;
	private static Logger log = Logger.getLogger(WMSMetaLayer.class);
	
	/**
	 * Contructs a {@link WMSMetaLayer} 
	 * @param multiplicity.1 = no meta-tiling, 2 = 4 tiles in one meta tile, 3 = 9 tiles in one meta tile, n = n^2 tiles in one meta tile
	 * @param wmsService
	 * @param cache
	 * @param resolutions
	 * @param tilesize
	 * @param coverage
	 * @param origin
	 * @param title
	 * @throws IllegalArgumentException if multiplicity is less than one.
	 */
	public WMSMetaLayer(int multiplicity, WMSService wmsService, Cache cache, ResolutionSet resolutions, TileFormat format , BBox coverage, Pos origin, String title) {
		super(wmsService, cache, resolutions, format, coverage, origin,
				title);
		if(multiplicity<=0)
		{
			throw new IllegalArgumentException("Multiplicity must be more than zero");
		}
		else
		{
			this.multiplicity=multiplicity;
		}
	}
	
	/**
	 * Copies a existing {@link WMSMetaLayer} using a new set of resolutions and a new coverage.
	 * @param layer existing AbstractLiveLayer
	 * @param resolutions new set of resolutions
	 * @param coverage new coverage area.
	 */
	protected WMSMetaLayer(final WMSMetaLayer layer, ResolutionSet resolutions, BBox coverage)
	{
		super(layer,resolutions,coverage);
		multiplicity = layer.multiplicity;
	}
	
	/**
	 * Gets the current multplicity being used.
	 * @return
	 */
	public final int getMultiplicity() {
		return multiplicity;
	}

	/**
	 * If tile is not in cache the meta tile for this tile is taken and all the tiles within the meta tile a cached.
	 * Tile is updated with the cached image.
	 */
	public void getTile(Tile tile) {
		if (tile instanceof MetaTile) {
			MetaTile metaTile = (MetaTile) tile;
			getMetaTile(metaTile);
		}
		else
		{
			BufferedImage old = tile.getImage();
			getCache().get(tile);
			if(tile.getImage()==old)
			{
				//Get metatile that contians the tile
				MetaTile metaTile = new MetaTile(tile,multiplicity);
				
				//Get and cache the meta-tile
				notInCacheAction(metaTile);
				
				//Now retry getting the tile image from cache
				getCache().get(tile);
			}
		}
	}
	
	/**

	 * to form a picture of the MetaTile.
	 */
	public void getMetaTile(MetaTile tile)
	{
		log.debug("Meta-Tile caching: " + Math.pow(multiplicity,2) + " tile-images to cache.");
		
		
		for(int x = 0; x < multiplicity; x++)
		{
			for(int y = 0; y < multiplicity; y++)
			{
				Tile smallerTile = new Tile(tile.getLayer(),tile.getZoom(),tile.getXIndex()*multiplicity + x, tile.getYIndex()*multiplicity + y);
				getCache().get(smallerTile);
				if(smallerTile.getImage()==null)
				{
					notInCacheAction(tile);
				}
			}
		}
	}
	
	/**
	 * Splits up the Meta Tile into chunks and caches all the smaller tiles
	 */
	protected void putInCacheAction(Tile tile)
	{
		if (tile instanceof MetaTile) {
			MetaTile bigTile = (MetaTile) tile;
		
			int w = bigTile.getOriginalTileSize().width;
			int h = bigTile.getOriginalTileSize().height;
			
			log.debug("Meta-Tile caching: " + Math.pow(multiplicity,2) + " tile-images to cache.");
			
			for(int x = 0; x < multiplicity; x++)
			{
				for(int y = 0; y < multiplicity; y++)
				{
					Tile smallerTile = new Tile(bigTile.getLayer(),bigTile.getZoom(),bigTile.getXIndex()*multiplicity + x, bigTile.getYIndex()*multiplicity + y);
					smallerTile.setImage(bigTile.getImage().getSubimage(x*w, (multiplicity-1-y)*h, w, h));
					getCache().set(smallerTile);
				}
			}
		}
		else
		{
			throw new ClassCastException("Meta-Layer tile was not a Meta-Tile. This cant be happening. Please contact lead developer to resolve this issue");
		}
	}
	
	/**
	 * <p>Gets a layer that makes iteration useful and easy. The new layer is still essentially "this" Layer,
	 * but with a smaller set of resolutions and a smaller coverage. When you iterate a layer, it will give each tile
	 * in the entire coverage at every zoom level. This can be up to Several Billion Tiles!!
	 * By reducing the coverage and zoom levels you can focus your iterations on a specific sub-set of tiles.</p>
	 * 
	 * <p>When/If you do this, be aware of how you match tiles "zoom" with layer "resolution". 
	 * The new layer has less resolutions, so a resolution that used to correspond to a zoom 
	 * (based on index in resolution set) may now correspond to a different zoom. 
	 * Make use of layer.getResolution() and layer.getZoom(). These will match the zooms and resolutions up
	 * properly.</p>
	 * 
	 * @param coverage
	 * @param minZoom
	 * @param maxZoom
	 * @return A smaller sub-set Layer.
	 */
	public WMSMetaLayer getSubCoverageLayer (BBox extent, int minZoom, int maxZoom)
	{
		final Resolution[] oldResolutions = getResolutions().toArray(new Resolution[this.getResolutions().size()]);
		ResolutionSet newResolutions = new ResolutionSet();
		for(int index = minZoom; index <= maxZoom; index++)
		{
			newResolutions.add(oldResolutions[index]);
		}

		WMSMetaLayer retLayer = new WMSMetaLayer(this,newResolutions,extent);
		return retLayer;
	}
	
	
	/**
	 * Gives an Iterable that iterates through MetaTiles rather than Tiles.
	 * @return
	 */
	public Iterable<MetaTile> getMetaIterable()
	{
		return new MetaLayerIterable(this);
	}

	/**
	 * Private class representing a bigger tile that splits into the normal size ones.
	 * 
	 * @author mvivian
	 */
	public class MetaTile extends Tile
	{
		
		public MetaTile(Tile tile, int multiplicity) {
			super(tile.getLayer(),tile.getZoom(),tile.getXIndex()/multiplicity,tile.getYIndex()/multiplicity);
		}
		
		public MetaTile(WMSMetaLayer layer, int zoom, int xIndex, int yIndex) {
			super(layer, zoom, xIndex, yIndex);
		}

		public Dimension getTileSize()
		{
			
			return new Dimension(super.getTileSize().width*multiplicity,super.getTileSize().height*multiplicity);
		}
		
		public Dimension getOriginalTileSize()
		{
			return super.getTileSize();
		}
		
		public final WMSMetaLayer getLayer() {
			return WMSMetaLayer.this;
		}
		
		public String toString()
		{
			return "MetaTile (x = " + getXIndex() + ", y = " + getYIndex() + ", z = " + getZoom() + ", multiplicy = " + multiplicity + ")";
		}
	}
}
